QuteBrowser Mode 模式管理架构实现
介绍
QuteBrowser 像 Vim 一样,也包含多种 Mode,modman 是 QuteBrowser 中 Mode 管理器。本文梳理 modman 的实现原理。
多少种 Mode?
参见 KeyMode 枚举:
class KeyMode(enum.Enum):
"""Key input modes."""
normal = enum.auto() #: Normal mode (no mode was entered)
hint = enum.auto() #: Hint mode (showing labels for links)
command = enum.auto() #: Command mode (after pressing the colon key)
yesno = enum.auto() #: Yes/No prompts
prompt = enum.auto() #: Text prompts
insert = enum.auto() #: Insert mode (passing through most keys)
passthrough = enum.auto() #: Passthrough mode (passing through all keys)
caret = enum.auto() #: Caret mode (moving cursor with keys)
set_mark = enum.auto()
jump_mark = enum.auto()
record_macro = enum.auto()
run_macro = enum.auto()
# 'register' is a bit of an oddball here: It's not really a "real" mode,
# but it's used in the config for common bindings for
# set_mark/jump_mark/record_macro/run_macro.
register = enum.auto()
在不同的 Mode 下,键盘的输入行为是不一样的。
Qt 键盘事件响应
在 app.py 中的 init 中,对事件过滤器进行初始化:
eventfilter.init()
具体实现:
def init() -> None:
"""Initialize the global EventFilter instance."""
event_filter = EventFilter(parent=objects.qapp)
event_filter.install()
quitter.instance.shutting_down.connect(event_filter.shutdown)
EventFilter
这是一个 Qt 对象,注册进入 QApp 中,接管了用户按键:
class EventFilter(QObject):
"""Global Qt event filter.
Attributes:
_activated: Whether the EventFilter is currently active.
_handlers: A {QEvent.Type: callable} dict with the handlers for an
event.
"""
def __init__(self, parent: QObject = None) -> None:
super().__init__(parent)
self._activated = True
self._handlers = {
QEvent.KeyPress: self._handle_key_event,
QEvent.KeyRelease: self._handle_key_event,
QEvent.ShortcutOverride: self._handle_key_event,
}
def install(self) -> None:
objects.qapp.installEventFilter(self)
_handle_key_event
该类还有一个 eventFilter 方法,用于响应事件,最终都会调入 _handle_key_event:
def _handle_key_event(self, event: QKeyEvent) -> bool:
"""Handle a key press/release event.
Args:
event: The QEvent which is about to be delivered.
Return:
True if the event should be filtered, False if it's passed through.
"""
active_window = objects.qapp.activeWindow()
if active_window not in objreg.window_registry.values():
# Some other window (print dialog, etc.) is focused so we pass the
# event through.
return False
try:
man = modeman.instance('current')
return man.handle_event(event)
except objreg.RegistryUnavailableError:
# No window available yet, or not a MainWindow
return False
由于每个 Window 都有一个 ModeManager,因此从中取出活跃的那个,把 event 分发进去。
ModeManager
键盘模式管理器,一个类。
属性
- mode:当前所属模式
- hintmanager:与当前窗口关联的提示管理器
- _win_id:窗口 id
- _prev_mode:prompt 弹出前的模式
- parsers:字典,mode 与 keyparsers 的映射
handle_event
根据当前设置的模式,在模式内进行分发:
def handle_event(self, event: QEvent) -> bool:
"""Filter all events based on the currently set mode.
Also calls the real keypress handler.
Args:
event: The KeyPress to examine.
Return:
True if event should be filtered, False otherwise.
"""
handlers: Mapping[QEvent.Type, Callable[[QKeyEvent], bool]] = {
QEvent.KeyPress: self._handle_keypress,
QEvent.KeyRelease: self._handle_keyrelease,
QEvent.ShortcutOverride:
functools.partial(self._handle_keypress, dry_run=True),
}
handler = handlers[event.type()]
return handler(cast(QKeyEvent, event))
_handle_keypress
响应按下事件,这是分发的最关键之处:
def _handle_keypress(self, event: QKeyEvent, *,
dry_run: bool = False) -> bool:
"""Handle filtering of KeyPress events.
处理键盘事件过滤
Args:
event: The KeyPress to examine. 事件
dry_run: Don't actually handle the key, only filter it 是否消费事件.
Return:
True if event should be filtered, False otherwise. 是否消费事件
"""
# 获取当前 Mode
curmode = self.mode
# 获取当前 Mode 对应的 Parser
parser = self.parsers[curmode]
if curmode != usertypes.KeyMode.insert:
log.modes.debug("got keypress in mode {} - delegating to "
"{}".format(curmode, utils.qualname(parser)))
# 由 Parser 是否匹配该事件
match = parser.handle(event, dry_run=dry_run)
# 是否是功能键
has_modifier = event.modifiers() not in [
Qt.NoModifier,
Qt.ShiftModifier,
] # type: ignore[comparison-overlap]
is_non_alnum = has_modifier or not event.text().strip()
forward_unbound_keys = config.cache['input.forward_unbound_keys']
if match:
filter_this = True
elif (parser.passthrough or forward_unbound_keys == 'all' or
(forward_unbound_keys == 'auto' and is_non_alnum)):
filter_this = False
else:
filter_this = True
if not filter_this and not dry_run:
self._releaseevents_to_pass.add(KeyEvent.from_event(event))
if curmode != usertypes.KeyMode.insert:
focus_widget = objects.qapp.focusWidget()
log.modes.debug("match: {}, forward_unbound_keys: {}, "
"passthrough: {}, is_non_alnum: {}, dry_run: {} "
"--> filter: {} (focused: {!r})".format(
match, forward_unbound_keys,
parser.passthrough, is_non_alnum, dry_run,
filter_this, focus_widget))
return filter_this
其中:
- _releaseevents_to_pass:每次按下一个键,都会将 Event 添加其中,等到键盘抬起时消费
- 问题:作用是什么,应该时用于去重的
单独的按键时怎么汇集成命令的?
这里有一个误解,我老想着的是按下 ":
" 输入命令的地方。
实际上这里驱动的也包括普通模式,也就是 hjkl
那些快捷键。
而按下 ":
" 输入命令的地方,应该是有一个组件负责接收。
Parsers
基类是 BaseKeyParser,派生出:
- CommandKeyParser
- HintKeyParser
Mode 与 Parser 映射
位于 init 方法中:
def init(win_id: int, parent: QObject) -> 'ModeManager':
"""Initialize the mode manager and the keyparsers for the given win_id."""
commandrunner = runners.CommandRunner(win_id)
modeman = ModeManager(win_id, parent)
objreg.register('mode-manager', modeman, scope='window', window=win_id)
hintmanager = hints.HintManager(win_id, parent=parent)
objreg.register('hintmanager', hintmanager, scope='window',
window=win_id, command_only=True)
modeman.hintmanager = hintmanager
log_sensitive_keys = 'log-sensitive-keys' in objects.debug_flags
keyparsers: ParserDictType = {
usertypes.KeyMode.normal:
modeparsers.NormalKeyParser(
win_id=win_id,
commandrunner=commandrunner,
parent=modeman),
usertypes.KeyMode.hint:
modeparsers.HintKeyParser(
win_id=win_id,
commandrunner=commandrunner,
hintmanager=hintmanager,
parent=modeman),
usertypes.KeyMode.insert:
modeparsers.CommandKeyParser(
mode=usertypes.KeyMode.insert,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman,
passthrough=True,
do_log=log_sensitive_keys,
supports_count=False),
usertypes.KeyMode.passthrough:
modeparsers.CommandKeyParser(
mode=usertypes.KeyMode.passthrough,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman,
passthrough=True,
do_log=log_sensitive_keys,
supports_count=False),
usertypes.KeyMode.command:
modeparsers.CommandKeyParser(
mode=usertypes.KeyMode.command,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman,
passthrough=True,
do_log=log_sensitive_keys,
supports_count=False),
usertypes.KeyMode.prompt:
modeparsers.CommandKeyParser(
mode=usertypes.KeyMode.prompt,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman,
passthrough=True,
do_log=log_sensitive_keys,
supports_count=False),
usertypes.KeyMode.yesno:
modeparsers.CommandKeyParser(
mode=usertypes.KeyMode.yesno,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman,
supports_count=False),
usertypes.KeyMode.caret:
modeparsers.CommandKeyParser(
mode=usertypes.KeyMode.caret,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman,
passthrough=True),
usertypes.KeyMode.set_mark:
modeparsers.RegisterKeyParser(
mode=usertypes.KeyMode.set_mark,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman),
usertypes.KeyMode.jump_mark:
modeparsers.RegisterKeyParser(
mode=usertypes.KeyMode.jump_mark,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman),
usertypes.KeyMode.record_macro:
modeparsers.RegisterKeyParser(
mode=usertypes.KeyMode.record_macro,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman),
usertypes.KeyMode.run_macro:
modeparsers.RegisterKeyParser(
mode=usertypes.KeyMode.run_macro,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman),
}
for mode, parser in keyparsers.items():
modeman.register(mode, parser)
return modeman
快捷键的注册表在哪里
研究了那么多,但是没看到哪些命令是怎么与案件关联起来的。
找了一圈让我找到了,原来在 qutebrowser\config\configdata.yml 里,通过配置文件来声明的。
在 qutebrowser\config\configdata.py 中会加载该配置文件:
def init() -> None:
"""Initialize configdata from the YAML file."""
global DATA, MIGRATIONS
DATA, MIGRATIONS = _read_yaml(resources.read_file('config/configdata.yml'))
总结
本文对 QuteBrowser 中键盘驱动系统的重点模块做了分析,但是很多地方还没有讲透。
比如各个模式作用是什么。另外也感觉键盘驱动系统跟 Mode 管理还不是一回事,可能需要拆除去一篇单独文章。
待后续持续完善。